Objects and Object Constructor
Resource
I. Object-literal syntax
There are multiple ways to define an object, but in most cases, it’s best to use object-literal syntax. Object-literal creates a single, unique object.
// object literal syntax
const myObject = {
property: 'Value!',
otherProperty: 77,
"my method": function() {
// do stuff!
}
};
We can use dot notation or bracket notation to access information from an object.
// dot notation
myObject.property; // 'Value!'
// bracket notation
myObject["my method"]; // [Function: my method]
Objects as a design pattern
Using objects as a design pattern helps organize and manage data more efficiently, especially as the complexity of your codebase increases.
Example: A game of Tic-Tac-Toe
- Without objects:
const playerOneName = "tim";
const playerTwoName = "jenn";
const playerOneMarker = "X";
const playerTwoMarker = "O";
- With objects: using object literals.
const playerOne = {
name: "tim",
marker: "X"
};
const playerTwo = {
name: "jenn",
marker: "O"
};
// Benefits of using objects:
// Function to print any player's name without needing to know the specific variable name
function printName(player) {
console.log(player.name);
}
// Function to announce the winner of the game
function gameOver(winningPlayer) {
console.log(`Congratulations! ${winningPlayer.name} is the winner!`);
}
// Call the functions with the objects as parameters
printName(playerOne); // Output: tim
gameOver(playerTwo); // Output: Congratulations! jenn is the winner!
→ The first approach might seem straightforward, but the object-based approach offers significant benefits.
II. Object constructors
When you want multiple instances of a specific type of object, a better way to create them is using an object constructor.
An object constructor is a special function in JavaScript used to create and initialize an object. It defines a blueprint for creating multiple instances of objects with the same structure.
→ Each instance of this object created by the object constructor will share the same properties and methods defined in the constructor, allowing for the efficient creation and management of objects with similar characteristics.
// declaring an object constructor initializing object Player
function Player(name, marker) {
this.name = name;
this.marker = marker;
}
// object constructor created using a function expression
// very UNCOMMON because this won't be hoisted
const Player = function(name, marker){
this.name = name;
this.marker = marker;
}
- You must use the keyword
newto create an object instance with the constructor, otherwise you’ll get unexpected behavior/errors.
// calling the function with the keyword `new`
// a new instance player1 of Player is created
const player1 = new Player('steve', 'X');
console.log(player1.name); // 'steve'
The object constructor syntax allows the creation of multiple instances of the same type of object, while the object literal syntax creates a single object.
// object literal
// create a single object
const player = {
name: 'steve',
marker: 'X',
sayName: function() {
console.log(this.name);
}
};
// object constructor syntax
// allows creation of multiple instances of the same object
function Player(name, marker) {
this.name = name;
this.marker = marker;
this.sayName = function() {
console.log(this.name);
};
}
const player1 = new Player('steve', 'X');
const player2 = new Player('also steve', 'O');
player1.sayName(); // logs 'steve'
player2.sayName(); // logs 'also steve'
a. this keyword
The this keyword in JavaScript is a special identifier that refers to the current execution context.
- Global context:
console.log(this === window); // true in a browser
this.globalVariable = "I'm global";
console.log(window.globalVariable); // "I'm global"
- Method call: When a function is called as a method of an object,
thisrefers to the object that owns the method.
const obj = {
method: function() {
console.log(this);
}
};
obj.method(); // `this` refers to `obj`
- Constructor call: When a function is used as a constructor with the
newkeyword,thisrefers to the newly created object.
function Person(name) {
this.name = name;
this.introduce = function() {
console.log(`My name is ${this.name}`);
};
}
const alice = new Person('Alice');
alice.introduce(); // Output: My name is Alice
- Arrow functions: Arrow functions don't have their own
this. They inheritthisfrom the enclosing lexical scope.
function createObjects() {
this.name = "Enclosing lexical scope";
return {
name: 'outer',
inner: {
name: 'inner',
regularMethod: function() {
console.log(`Regular: ${this.name}`);
},
arrowMethod: () => {
console.log(`Arrow: ${this.name}`);
}
}
};
}
const outerObject = createObjects.call({});
outerObject.inner.regularMethod(); // "Regular: inner"
outerObject.inner.arrowMethod(); // "Arrow: Enclosing lexical scope"
Note: Using arrow functions as constructors is not recommended and will not work as expected. Arrow functions do not have their own
thiscontext, which is crucial for constructors.
b. call method
The call method is a built-in JavaScript method that allows you to call a function with a specified this value and arguments. In other words, it lets you control what this refers to when executing a function.
// syntax
functionName.call(thisValue, arg1, arg2, ...)
1. Basic Usage
Example: Even though greet() is a standalone function, call allowed us to run it as if it’s a part of the person object.
function greet() {
console.log(`Hello, my name is ${this.name}`);
}
const person = {
name: "Alice"
};
// Using call to set 'this' to the person object
greet.call(person); // Output: "Hello, my name is Alice"
2. With Constructor Functions
Example: call is often used with constructor functions to share initialization of code. In this example, the Student constructor borrows the Person constructor’s functionality.
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(name, age, grade) {
// Call the Person constructor with the current 'this'
Person.call(this, name, age);
this.grade = grade;
}
const student1 = new Student("Bob", 15, "10th");
console.log(student1);
// Output: { name: "Bob", age: 15, grade: "10th" }
3. Sharing Methods
Example:const calculator = {
multiply: function(a, b) {
return a * b;
}
};
const scientificCalc = {
square: function(x) {
// Use multiply method from calculator
return calculator.multiply.call(this, x, x);
}
};
console.log(scientificCalc.square(4)); // Output: 16
III. The prototype
Resource
A prototype is another object that the original object inherits from, meaning the original object has access to all its prototype's methods and properties.
a. All objects in JavaScript have a prototype.
- You can check the object’s
prototypeby using theObject.getPrototypeOf()function on the object, e.g.Object.getPrototypeOf(person1).
b. The prototype itself is an object…
When you create a new Person object (like person1 or person2), JavaScript automatically sets up a link between the newly created objects and Person.prototype. This link is what we call the object's prototype.
- Hence, you get a
truevalue returned when you check the Objects prototype -Object.getPrototypeOf(player1) === Player.prototype.
c. …that the original object inherits from, and has access to all of its prototype’s methods and properties
- This means that any methods or properties defined on
Person.prototypeare accessible to all objects (person1,person2) created from thePersonconstructor.
Example:
- We define a
Personconstructor function. - We define the
.sayHellofunction on thePlayer.prototypeobject.
→ It then became available for the player1 and the player2 objects to use.
Similarly, you can attach other properties or functions you want to use on all Player objects by defining them on the objects’ prototype (Player.prototype).
// create an object with object constructor
function Person(name) {
this.name = name;
}
// Adding a method to the Person.prototype
Person.prototype.sayHello = function() {
console.log("Hello, I'm " + this.name);
};
// Creating objects
const person1 = new Person("Alice");
const person2 = new Person("Bob");
// Using the prototype method
person1.sayHello(); // Output: Hello, I'm Alice
person2.sayHello(); // Output: Hello, I'm Bob
// Checking the prototype
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(person2) === Person.prototype); // true
To further illustrate:
console.log(person1.hasOwnProperty('sayHello')); // false
console.log('sayHello' in person1); // true
→ This shows that sayHello isn't a direct property of person1, but person1 can still access it through its prototype.
1. Object.getPrototypeOf() vs. .__proto__ vs. [[Prototype]] vs. instanceof
Resource
a. Object.getPrototypeOf()
You can check the object’s immediate prototype by using the Object.getPrototypeOf() method on the object. This is the recommended way.
Object.getPrototypeOf(player1) === Player.prototype; // true
Object.getPrototypeOf(player2) === Player.prototype; // true
b. ._proto_
Unlike using Object.getPrototypeOf() to access an object’s prototype, the same thing can also be done using the .__proto__ property of the object. However, this is a non-standard way of doing so and is deprecated. Hence, it is NOT recommended to access an object’s prototype by using this property.
// Don't do this!
player1.__proto__ === Player.prototype; // returns true
player2.__proto__ === Player.prototype; // returns true
c.[[Prototype]]
In some places, you might come across [[Prototype]], which is another way of talking about the .__proto__ property of an object, like player1.[[Prototype]].
d. instanceof operator
You can use instanceof to check if an object is an instance of a particular class or constructor function. The operator checks the entire chain (unlike Object.getPrototypeOf() that only checks the one level up, immediate prototype).
You can implement instanceof using Object.getPrototypeOf():
function isInstanceOf(object, constructor) {
let proto = Object.getPrototypeOf(object);
while (proto !== null) {
if (proto === constructor.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
// Usage
const arr = [];
console.log(isInstanceOf(arr, Array)); // true
console.log(isInstanceOf(arr, Object)); // true
// array protoype chain
arr ---> Array.prototype ---> Object.prototype ---> null
// tesla prototype chain
tesla ---> Car.prototype ---> Object.prototype ---> null
const arr = [];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true (arrays inherit from Object)
// object constructor named Car
function Car(brand){
this.brand = brand;
}
const tesla = new Car('Tesla');
console.log(tesla instanceof Car); // true
console.log(tesla instanceof Object); // true
You can verify this chain using Object.getPrototypeOf():
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(tesla) === Car.prototype); // true
console.log(Object.getPrototypeOf(Car.prototype) === Object.prototype); // true
2. Prototypal Inheritance*
Resource
Prototypal Inheritance is when objects can access and use properties/methods from their prototype objects. When an object is created, it is automatically linked to its prototype object.
Every
prototypeobject inherits fromObject.prototypeby default.
Example:
player1andplayer2inherit fromPlayer.prototype.Player.prototypeitself inherits fromObject.prototype, like everyprototypeobject.- This forms a chain:
player1->Player.prototype->Object.prototype
// Player.prototype.__proto__
Object.getPrototypeOf(Player.prototype) === Object.prototype; // true
// Output may slightly differ based on the browser
player1.valueOf(); // Output: Object { name: "steve", marker: "X", sayName: sayName() }
In this example, we are using the valueOf() function, we didn’t define it but it is defined on Object.prototype. player1 object inherits this function from Player.prototype, which inherits it from Object.prototype.
player1.hasOwnProperty('valueOf'); // false - because hasOwnProperty looks for the direct method, not inherited
Object.prototype.hasOwnProperty('valueOf'); // true
Same with
.hasOwnPropertyfunction, this function is also defined on theObject.prototype.hasOwnPropertychecks if the property is directly on the object itself (not inherited)
Object.prototype.hasOwnProperty('hasOwnProperty'); // true
This is how JavaScript utilizes
prototype- by having the objects contain a value - to point toprototypes and inherit from those prototypes, thus forming a chain. JavaScript figures out which properties exist (or do not exist) on the object and starts traversing the chain to find the property or function.
However, this chain does not go on forever. If you have already tried logging the value of
Object.getPrototypeOf(Object.prototype), you will find that it isnull, which indicates the end of the chain. At the end of this chain, if the specific property or function is not found,undefinedis returned.
a. Defining Methods in Object Constructor
function Player(name, marker) {
this.name = name;
this.marker = marker;
this.sayName = function() { // Method defined inside the constructor
console.log(this.name);
};
}
const player1 = new Player('steve', 'X');
const player2 = new Player('also steve', 'O');
The sayName function is defined for each instance of Player. This means every single instance of Player (like player1 and player2) will have its copy of the sayName method.
While this works, it could be more memory-efficient, especially if you want to create many instances of Player, because then each instance will have its own copy of sayName.
b. Defining Methods in Prototype
function Player(name, marker) {
this.name = name;
this.marker = marker;
}
// Adding the method to the prototype
Player.prototype.sayName = function() { // Method defined on the prototype
console.log(this.name);
};
const player1 = new Player('steve', 'X');
const player2 = new Player('also steve', 'O');
The sayName function is shared by all instances of Player. When you create player1 and player2, they will both reference the same sayName method defined on Player.prototype, rather than having their own copies of it.
→ Defining property and function takes up a lot of memory (especially if you have a lot of common properties and functions), and a lot of created objects. That’s why, defining them on a centralized, property object that the objects have access to is much more memory-efficient.
3. Recommended method for prototypal inheritance
In this example:
- We define a
Personfrom whom aPlayerinherits properties and functions. - We use
Object.setPrototypeOf()is used to establish this inheritance relationship betweenPlayerandPerson. - Then, we create object instances
player1andplayer2which inherits from bothPersonandPlayer.player1andplayer2both have access togetMarker(),sayName()methods.- Use methods like
toString()fromObject.prototype().
// prototype chain
player1, player2 ---> Player.prototype --> Person.prototype --> Object.prototype ---> null
// object constructor - defines Person object
function Person(name) {
this.name = name;
}
// object constructor - defines Player object
function Player(name, marker) {
this.name = name;
this.marker = marker;
}
// defines the sayName method on Person.prototype
Person.prototype.sayName = function() {
console.log(`Hello, I'm ${this.name}!`);
};
// defines the getMarker method on Player.prototype
Player.prototype.getMarker = function() {
console.log(`My marker is '${this.marker}'`);
};
Object.getPrototypeOf(Player.prototype); // returns Object.prototype
// Now make `Player` objects inherit from `Person`
// do this BEFORE creating new object instances
Object.setPrototypeOf(Player.prototype, Person.prototype);
Object.getPrototypeOf(Player.prototype); // returns Person.prototype
// create object instances
const player1 = new Player('steve', 'X');
const player2 = new Player('also steve', 'O');
// player1 also inherits sayName method from Person
player1.sayName(); // Hello, I'm steve!
player2.sayName(); // Hello, I'm also steve!
// player1 inherits getMarker method from Player
player1.getMarker(); // My marker is 'X'
player2.getMarker(); // My marker is 'O'
setPrototypeOf()
Though it seems easy to set up prototypal inheritance using Object.setPrototypeOf(), the prototype chain must be set up using this function before creating any objects. Using setPrototypeOf() after objects instances have already been created can result in performance issues.
Example 1: This will set Player.prototype to directly refer to Person.prototype (i.e. not a copy). Any changes you make to the Person.prototype will get reflected in Player.prototype.
// this does not work
// DO NOT, EVER reassign like this
Player.prototype = Person.prototype;
Example 2: If we had used Object.setPrototypeOf() in this example, then we could safely edit the Enemy.prototype.sayName function without changing the function for Player as well.
function Person(name) {
this.name = name;
}
function Player(name, marker) {
this.name = name;
this.marker = marker;
}
Person.prototype.sayName = function() {
console.log(`Hello, I'm ${this.name}!`);
};
// Don't do this!
// Use Object.setPrototypeOf(Player.prototype, Person.prototype)
Player.prototype = Person.prototype;
// another object constructor Enemy
function Enemy(name) {
this.name = name;
this.marker = '^';
}
// Not again!
// Use Object.setPrototypeOf(Enemy.prototype, Person.prototype)
Enemy.prototype = Person.prototype;
Enemy.prototype.sayName = function() {
console.log('HAHAHAHAHAHA');
};
const carl = new Player('carl', 'X');
carl.sayName(); // Uh oh! this logs "HAHAHAHAHAHA" because we edited the sayName function!